/*
 * ProGuard -- shrinking, optimization, obfuscation, and preverification of Java bytecode.
 *
 * Copyright (c) 2002-2018 GuardSquare NV
 *
 * This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public
 * License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later
 * version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
 * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along with this program; if not, write to the Free
 * Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 */
package proguard;

import java.util.List;

import proguard.classfile.ClassConstants;
import proguard.classfile.ClassPool;
import proguard.io.CascadingDataEntryWriter;
import proguard.io.ClassDataEntryWriter;
import proguard.io.DataEntryNameFilter;
import proguard.io.DataEntryParentFilter;
import proguard.io.DataEntryWriter;
import proguard.io.DirectoryWriter;
import proguard.io.ExtraDataEntryWriter;
import proguard.io.FilteredDataEntryWriter;
import proguard.io.JarWriter;
import proguard.io.ParentDataEntryWriter;
import proguard.io.PrefixAddingDataEntryWriter;
import proguard.util.ExtensionMatcher;
import proguard.util.FileNameParser;
import proguard.util.ListParser;
import proguard.util.MultiValueMap;

/**
 * This class can create DataEntryWriter instances based on class paths. The writers will wrap the output in the proper
 * apks, jars, wars, ears, jmods, and zips.
 *
 * @author Eric Lafortune
 */
public class DataEntryWriterFactory {
    private final ClassPool programClassPool;
    private final MultiValueMap extraClassNameMap;

    /**
     * Creates a new DataEntryWriterFactory with the given parameters.
     * 
     * @param programClassPool the program classpool to process.
     */
    public DataEntryWriterFactory(ClassPool programClassPool, MultiValueMap<String, String> extraClassNamemap) {
        this.programClassPool = programClassPool;
        this.extraClassNameMap = extraClassNamemap;
    }

    /**
     * Creates a DataEntryWriter that can write to the given class path entries.
     *
     * @param classPath the output class path.
     * @param fromIndex the start index in the class path.
     * @param toIndex the end index in the class path.
     * @return a DataEntryWriter for writing to the given class path entries.
     */
    public DataEntryWriter createDataEntryWriter(ClassPath classPath, int fromIndex, int toIndex) {
        DataEntryWriter writer = null;

        // Create a chain of writers, one for each class path entry.
        for (int index = toIndex - 1; index >= fromIndex; index--) {
            ClassPathEntry entry = classPath.get(index);

            writer = createClassPathEntryWriter(entry, writer);
        }

        return writer;
    }

    /**
     * Creates a DataEntryWriter that can write to the given class path entry, or delegate to another DataEntryWriter if
     * its filters don't match.
     */
    private DataEntryWriter createClassPathEntryWriter(ClassPathEntry classPathEntry,
        DataEntryWriter alternativeWriter) {
        boolean isApk = classPathEntry.isApk();
        boolean isJar = classPathEntry.isJar();
        boolean isAar = classPathEntry.isAar();
        boolean isWar = classPathEntry.isWar();
        boolean isEar = classPathEntry.isEar();
        boolean isJmod = classPathEntry.isJmod();
        boolean isZip = classPathEntry.isZip();

        List filter = classPathEntry.getFilter();
        List apkFilter = classPathEntry.getApkFilter();
        List jarFilter = classPathEntry.getJarFilter();
        List aarFilter = classPathEntry.getAarFilter();
        List warFilter = classPathEntry.getWarFilter();
        List earFilter = classPathEntry.getEarFilter();
        List jmodFilter = classPathEntry.getJmodFilter();
        List zipFilter = classPathEntry.getZipFilter();

        System.out
            .println(
                "Preparing output "
                    + (isApk ? "apk"
                        : isJar ? "jar"
                            : isAar ? "aar"
                                : isWar ? "war" : isEar ? "ear" : isJmod ? "jmod" : isZip ? "zip" : "directory")
                    + " [" + classPathEntry.getName() + "]"
                    + (filter != null || apkFilter != null || jarFilter != null || aarFilter != null
                        || warFilter != null || earFilter != null || jmodFilter != null || zipFilter != null
                            ? " (filtered)" : ""));

        DataEntryWriter writer =
            new DirectoryWriter(classPathEntry.getFile(), isApk || isJar || isAar || isWar || isEar || isJmod || isZip);

        // If the output is an archive, we'll flatten (unpack the contents of)
        // higher level input archives, e.g. when writing into a jar file, we
        // flatten zip files.
        boolean flattenApks = false;
        boolean flattenJars = flattenApks || isApk;
        boolean flattenAars = flattenJars || isJar;
        boolean flattenWars = flattenAars || isAar;
        boolean flattenEars = flattenWars || isWar;
        boolean flattenJmods = flattenEars || isEar;
        boolean flattenZips = flattenJmods || isJmod;

        // Set up the filtered jar writers.
        writer = wrapInJarWriter(writer, flattenZips, isZip, ".zip", zipFilter, null, null);
        writer = wrapInJarWriter(writer, flattenJmods, isJmod, ".jmod", jmodFilter, ClassConstants.JMOD_HEADER,
            ClassConstants.JMOD_CLASS_FILE_PREFIX);
        writer = wrapInJarWriter(writer, flattenEars, isEar, ".ear", earFilter, null, null);
        writer =
            wrapInJarWriter(writer, flattenWars, isWar, ".war", warFilter, null, ClassConstants.WAR_CLASS_FILE_PREFIX);
        writer = wrapInJarWriter(writer, flattenAars, isAar, ".aar", aarFilter, null, null);
        writer = wrapInJarWriter(writer, flattenJars, isJar, ".jar", jarFilter, null, null);
        writer = wrapInJarWriter(writer, flattenApks, isApk, ".apk", apkFilter, null, null);

        // Set up for writing out the program classes.
        writer = new ClassDataEntryWriter(programClassPool, writer);

        // Add a data entry filter, if specified.
        writer = filter != null ? new FilteredDataEntryWriter(
            new DataEntryNameFilter(new ListParser(new FileNameParser()).parse(filter)), writer) : writer;

        // Add a writer for the injected classes.
        writer = new ExtraDataEntryWriter(extraClassNameMap, writer, writer, ClassConstants.CLASS_FILE_EXTENSION);

        // Let the writer cascade, if specified.
        return alternativeWriter != null ? new CascadingDataEntryWriter(writer, alternativeWriter) : writer;
    }

    /**
     * Wraps the given DataEntryWriter in a JarWriter, filtering if necessary.
     */
    private DataEntryWriter wrapInJarWriter(DataEntryWriter writer, boolean flatten, boolean isOutputJar,
        String jarFilterExtension, List jarFilter, byte[] jarHeader, String classFilePrefix) {
        // Flatten jars or zip them up.
        DataEntryWriter jarWriter;
        if (flatten) {
            // Unpack the jar.
            jarWriter = new ParentDataEntryWriter(writer);
        } else {
            // Pack the jar.
            jarWriter = new JarWriter(jarHeader, writer);

            // Add a prefix for class files inside the jar, if specified.
            if (classFilePrefix != null) {
                jarWriter = new FilteredDataEntryWriter(
                    new DataEntryNameFilter(new ExtensionMatcher(ClassConstants.CLASS_FILE_EXTENSION)),
                    new PrefixAddingDataEntryWriter(classFilePrefix, jarWriter), jarWriter);
            }
        }

        // Either zip up the jar or delegate to the original writer.
        return new FilteredDataEntryWriter(
            new DataEntryParentFilter(new DataEntryNameFilter(new ExtensionMatcher(jarFilterExtension))),

            // The parent of the data entry is a jar.
            // Write the data entry to the jar.
            // Apply the jar filter, if specified, to the parent.
            jarFilter != null
                ? new FilteredDataEntryWriter(new DataEntryParentFilter(
                    new DataEntryNameFilter(new ListParser(new FileNameParser()).parse(jarFilter))), jarWriter)
                : jarWriter,

            // The parent of the data entry is not a jar.
            // Write the entry to a jar anyway if the output is a jar.
            // Otherwise just delegate to the original writer.
            isOutputJar ? jarWriter : writer);
    }
}
